/* 
 * Copyright (c) 2004-2005 Massachusetts Institute of Technology. This code was
 * developed as part of the Haystack (http://haystack.lcs.mit.edu/) research 
 * project at MIT. Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation files (the 
 * "Software"), to deal in the Software without restriction, including without 
 * limitation the rights to use, copy, modify, merge, publish, distribute, 
 * sublicense, and/or sell copies of the Software, and to permit persons to whom
 * the Software is furnished to do so, subject to the following conditions: 
 * 
 * The above copyright notice and this permission notice shall be included in 
 * all copies or substantial portions of the Software. 
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER  
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE. 
 */
/*
 * Created on Jun 28, 2004
 *
 */
package edu.mit.csail.relo.console;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PipedReader;
import java.io.PipedWriter;
import java.io.PrintStream;
import java.io.Reader;
import java.io.Writer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

import org.codehaus.groovy.control.CompilationFailedException;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.ITextListener;
import org.eclipse.jface.text.TextEvent;
import org.eclipse.jface.text.TextViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.part.ViewPart;


import groovy.lang.Binding;
import groovy.lang.GroovyRuntimeException;
import groovy.lang.GroovyShell;


public class ConsoleView extends ViewPart {
    
    // the shared instance of the view
	private static ConsoleView sharedView = null;
    
	public ConsoleView() {
	    super();
	    sharedView = this;
	}


	TextViewer scriptTextViewer = null;
	StyledText scriptTextWidget = null; 
	Document consoleDoc = new Document();
	Display display = null;
	
	/**
	 * This is a callback that will allow us
	 * to create the viewer and initialize it.
	 */
	@Override
    public void createPartControl(Composite parent) {
		scriptTextViewer =
			new TextViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
		scriptTextWidget = scriptTextViewer.getTextWidget();
		display = scriptTextWidget.getDisplay();
		
		scriptTextWidget.setDoubleClickEnabled(true);
		scriptTextWidget.setFont(JFaceResources.getTextFont());
		scriptTextWidget.setEditable(true);
		scriptTextWidget.setTabs(4);
		scriptTextViewer.setDocument(consoleDoc);
		scriptTextWidget.setForeground(display.getSystemColor(SWT.COLOR_BLUE));
		
		/*
        FontData[] curFontData = scriptTextWidget.getFont().getFontData();
        for (int i=0;i<curFontData.length; i++) {
            curFontData[i].setHeight(curFontData[i].getHeight()*80/100);
        }
        Font newFont = new Font(null, curFontData);
        scriptTextWidget.setFont(newFont);
        */
		
		consoleDoc.addDocumentListener( new IDocumentListener() {
			public void documentAboutToBeChanged(DocumentEvent e) {
			}
			public void documentChanged(DocumentEvent e) {
				 // Reveal (makes visible) the end of the current document
				 // From ConsoleView...
				int lines = consoleDoc.getNumberOfLines();
				try {
					// lines are 0-based
					int lineStartOffset = consoleDoc.getLineOffset(lines - 1);
					if (lineStartOffset > 0) {
						scriptTextWidget.setCaretOffset(lineStartOffset);
						scriptTextWidget.showSelection();
					}
					int lineEndOffset = lineStartOffset + consoleDoc.getLineLength(lines - 1);
					if (lineEndOffset > 0) {
						scriptTextWidget.setCaretOffset(lineEndOffset);
					}
				} catch (BadLocationException ex) {
				}
			}
		});

		initInputReader();
		initOutputStream();
		initPromptStream();
		initDbgStream();
		initErrorStream();
	    initGroovy();
	    
	    connectInterpreter();
	    
	    interpreter.set("help", new HelpSupport(getOutputStream()),
                "Provides scripting help; use 'help.methods help' for help commands available");
	}
	
	
	protected boolean outputting = false;
	
	/*
	 * write to this if you want to feed into the input stream, only person 
	 * really writing to this should be an input listener
	 */
	private Writer inputWriter = null;
	
	protected Reader inputRdr = null;
	// listens to widget and pipes the entered character to given inputWriter
	class WidgetInputListener implements ITextListener, KeyListener {
		int curBegNdx = -1;
        public void textChanged(TextEvent te) {
			//String repl = te.getReplacedText();
			//String givn = te.getText();

			if (outputting) {
				return;
			}
			if (curBegNdx == -1) {
				curBegNdx = te.getOffset();
			}
        }
        public void keyPressed(KeyEvent e) {
            if (e.keyCode == SWT.CR) {
                if ((e.stateMask & SWT.CTRL) != 0) {
                    // don't send
                    // allow user to type multi-line commands and edit it
                } else {
                    // send buffer
        			try {
        				interpreter.write(consoleDoc.get(curBegNdx, scriptTextWidget.getCaretOffset() - curBegNdx));
        				curBegNdx = -1;
        			} catch (BadLocationException ex) {
        				ex.printStackTrace();
        			}
                }
            }
        }
        public void keyReleased(KeyEvent e) {}
	}
	protected void initInputReader() {
		inputRdr = new PipedReader();
	    inputWriter = new PipedWriter();
		try {
		    ((PipedWriter)inputWriter).connect((PipedReader) inputRdr);
		} catch (IOException e) {
			e.printStackTrace();
		}
		WidgetInputListener wil = new WidgetInputListener();
		scriptTextWidget.addKeyListener(wil);
		scriptTextViewer.addTextListener(wil);
	}

	
	private boolean newDebugOutput = false;
	protected void widgetOutput(final String str, final int len, final int color, final boolean dbg) {
		outputting = true;
	    display.syncExec(new Runnable() {
			public void run() {
				int curNdx = scriptTextWidget.getCharCount();
				if (!dbg && newDebugOutput) {
					scriptTextWidget.append("\n");
				}
				scriptTextWidget.append(str);
				scriptTextWidget.setStyleRange(new StyleRange(curNdx, len, display.getSystemColor(color), null));
			}});
		outputting = false;
	}

	protected PrintStream outputStream = null;
	protected void initOutputStream() {
	    OutputStream tmpStream = new OutputStream() {
			@Override
            public void write(final int arg0) {
                widgetOutput(new String(new byte[] {(byte) arg0}), 1, SWT.COLOR_BLACK, false);
			}
			@Override
            public void write(final byte[] b, final int off, final int len) {
				byte[] tmpBuf = new byte[len];
                System.arraycopy(b, off, tmpBuf, 0, len);
                widgetOutput(new String(tmpBuf), len, SWT.COLOR_BLACK, false);
			}
		};
		outputStream = new PrintStream(tmpStream);
	}

	protected PrintStream promptStream = null;
	protected void initPromptStream() {
	    OutputStream tmpStream = new OutputStream() {
			@Override
            public void write(final int arg0) {
                widgetOutput(new String(new byte[] {(byte) arg0}), 1, SWT.COLOR_DARK_MAGENTA, false);
			}
			@Override
            public void write(final byte[] b, final int off, final int len) {
				byte[] tmpBuf = new byte[len];
                System.arraycopy(b, off, tmpBuf, 0, len);
                widgetOutput(new String(tmpBuf), len, SWT.COLOR_DARK_RED, false);
			}
		};
		promptStream = new PrintStream(tmpStream);
	}

	
	protected PrintStream debugStream = null;
	protected void initDbgStream() {
		debugStream = new PrintStream(new OutputStream() {
			@Override
            public void write(final int arg0) {
                widgetOutput(new String(new byte[] {(byte) arg0}), 1, SWT.COLOR_DARK_GRAY, true);
			    newDebugOutput = true;
			}
			@Override
            public void write(final byte[] b, final int off, final int len) {
				byte[] tmpBuf = new byte[len];
                System.arraycopy(b, off, tmpBuf, 0, len);
                widgetOutput(new String(tmpBuf), len, SWT.COLOR_DARK_GRAY, true);
			    newDebugOutput = true;
			}
		});
	}

	protected PrintStream errorStream = null;
	protected void initErrorStream() {
	    OutputStream tmpStream = new OutputStream() {
			@Override
            public void write(final int arg0) {
                widgetOutput(new String(new byte[] {(byte) arg0}), 1, SWT.COLOR_RED, false);
			}
			@Override
            public void write(final byte[] b, final int off, final int len) {
				byte[] tmpBuf = new byte[len];
                System.arraycopy(b, off, tmpBuf, 0, len);
                widgetOutput(new String(tmpBuf), len, SWT.COLOR_RED, false);
			}
		};
        errorStream = new PrintStream(tmpStream);
	}
	
	
	public static Reader getInputReader() {
	    if (sharedView != null) {
			return sharedView.inputRdr;
	    }
	    return null;
	}

	public static PrintStream getOutputStream() {
	    if (sharedView != null) {
			return sharedView.outputStream;
	    }
	    return null;
	}

	public static PrintStream getPromptStream() {
	    if (sharedView != null) {
			return sharedView.promptStream;
	    }
	    return null;
	}

	public static PrintStream getDebugStream() {
	    if (sharedView != null) {
			return sharedView.debugStream;
	    }
	    return null;
	}
	
	public static PrintStream getErrorStream() {
	    if (sharedView != null) {
			return sharedView.errorStream;
	    }
	    return null;
	}
	
	
	@Override
    public void setFocus() {
	    if (newDebugOutput) {
		    debugStream.println("");
		    newDebugOutput = false;
	    }
	    scriptTextWidget.setFocus();
	}

	
	
	// bshInterpreter support
	abstract static class PDEInterpreter {
	    // sets variables
        abstract void set(String arg, Object val, String varHelpStr);
        
        // writes to the interpreter input
        abstract void write(String string);
	};

	// suport for buffering variables before console is opened
	private static Map<String,ObjectPair> varBuffer = new HashMap<String,ObjectPair> ();
	private static class ObjectPair {
		public Object l;
		public Object r;
		public ObjectPair(Object l, Object r) {
			this.l = l;
			this.r = r;
		}
	}
	private static PDEInterpreter interpreter = new PDEInterpreter() {
        @Override
        public void set(String arg, Object val, String varHelpStr) {
            varBuffer.put(arg, new ObjectPair(val, varHelpStr));
        }
        @Override
        public void write(String string) {
        }
    };
    private void connectInterpreter() {
	    Iterator varIt = varBuffer.entrySet().iterator();
        while (varIt.hasNext()) {
            Map.Entry iME = (Entry) varIt.next();
            ObjectPair valPair = (ObjectPair) iME.getValue();
            interpreter.set((String) iME.getKey(), valPair.l, (String) valPair.r);
        }
        varBuffer = null;	// free mem.
    }
	
	
	
	protected void initGroovy() {
	    groovy.lang.MetaClass.setUseReflection(true);
	    Binding binding = new Binding();
	    ClassLoader parent = getClass().getClassLoader();
		final GroovyShell groovyShell = new GroovyShell(parent, binding);
		getPromptStream().print("grv % ");
		interpreter = new PDEInterpreter() {
            @Override
            public void set(String arg, Object val, String varHelpStr) {
                if (varHelpStr != null) {
                    getOutputStream().println("Setting: " + arg + " - " + varHelpStr);
                } else {
                    getOutputStream().println("Setting: " + arg + " : " + val);
                }
                groovyShell.setVariable(arg, val);
            }

            @Override
            public void write(String string) {
                // groovy does not support redirection of streams internally, 
                // so we have to do it here
                PrintStream oldOut = System.out;
                PrintStream oldErr = System.err;
                try {
                    System.setOut(getOutputStream());
                    System.setErr(getErrorStream());
                    Object retVal = groovyShell.evaluate(string);
                    if (retVal != null) {
                        getOutputStream().println(retVal);
                    }
                } catch (CompilationFailedException e) {
                    getErrorStream().println(e.toString());
                } catch (GroovyRuntimeException e) {
                    getErrorStream().println(e.toString());
                } catch (IOException e) {
                    getErrorStream().println("Unexpected Error!");
                    e.printStackTrace(oldErr);
                } catch (Throwable e) {
                    getErrorStream().println("Unexpected Error!");
                    e.printStackTrace(oldErr);
                } finally {
                    System.setOut(oldOut);
                    System.setErr(oldErr);
                    getPromptStream().print("grv % ");
                }
            }
		    
		};
	}
	
	public static void setVariable(String arg, Object val) {
		if (sharedView == null) {
            return;
        }
        interpreter.set(arg, val, null);
	}
	public static void setVariable(String arg, Object val, String varHelpStr) {
		if (sharedView == null) {
            return;
        }
        interpreter.set(arg, val, varHelpStr);
	}

	
	// debugging support
	
	// support nestings
	static int indentCnt = 0;

	// support for auto collapsing empty nestings
	static boolean indentSet = false;

	public static void logCause(Throwable t) {
	    logCause(t,4);
	}
	public static void logCause(Throwable t, int len) {
        StackTraceElement[] ste = t.getStackTrace();
        if (len == -1 || len > ste.length) {
            len = ste.length;
        }
        // 0: where t was instantiated
        // 1: who called t
        for (int i = 1; i < len; i++) {
            log("\t..." + ste[i].toString());
        }
    }
	
	public static void log(String t) {
		if (sharedView == null) {
			return;
		}
		sharedView.debugStream.println();
		for (int i=0;i<indentCnt;i++) {
			sharedView.debugStream.print("\t");
		}
		sharedView.debugStream.print(t);
		indentSet = false;
	}

	public static void logBeg(String t) {
		if (sharedView == null) {
			return;
		}

		log(t+" >>>");
		indentCnt++;	
		indentSet = true;
	}

	public static void logEnd() {
		if (sharedView == null) {
			return;
		}

		indentCnt--;
		if (indentSet == true) {
			sharedView.debugStream.print("<<<");
			indentSet = false;
		} else {
			sharedView.debugStream.print("<<<");
		}
	}
	
}